package com.hqd.ch03.v48.boot.context.config;

import com.hqd.ch03.utils.CollectionUtils;
import com.hqd.ch03.utils.LinkedMultiValueMap;
import com.hqd.ch03.utils.MultiValueMap;
import com.hqd.ch03.v48.boot.context.properties.bind.BindResult;
import com.hqd.ch03.v48.boot.context.properties.bind.Bindable;
import com.hqd.ch03.v48.boot.context.properties.bind.Binder;
import com.hqd.ch03.v48.boot.context.properties.source.ConfigurationPropertyName;
import com.hqd.ch03.v48.core.ResolvableType;
import com.hqd.ch03.v48.core.env.AbstractEnvironment;
import com.hqd.ch03.v48.core.env.Environment;
import com.hqd.ch03.v48.core.style.ToStringCreator;
import com.hqd.ch03.v48.utils.StringUtils;

import java.util.*;
import java.util.function.Function;

public class Profiles implements Iterable<String> {

    /**
     * Name of property to set to specify additionally included active profiles.
     */
    public static final String INCLUDE_PROFILES_PROPERTY_NAME = "spring.profiles.include";

    static final ConfigurationPropertyName INCLUDE_PROFILES = ConfigurationPropertyName
            .of(Profiles.INCLUDE_PROFILES_PROPERTY_NAME);

    private static final Bindable<MultiValueMap<String, String>> STRING_STRINGS_MAP = Bindable
            .of(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class));

    private static final Bindable<Set<String>> STRING_SET = Bindable.setOf(String.class);

    private final MultiValueMap<String, String> groups;

    private final List<String> activeProfiles;

    private final List<String> defaultProfiles;

    /**
     * Create a new {@link Profiles} instance based on the {@link Environment} and
     * {@link Binder}.
     *
     * @param environment        the source environment
     * @param binder             the binder for profile properties
     * @param additionalProfiles any additional active profiles
     */
    Profiles(Environment environment, Binder binder, Collection<String> additionalProfiles) {
        this.groups = binder.bind("spring.profiles.group", STRING_STRINGS_MAP).orElseGet(LinkedMultiValueMap::new);
        this.activeProfiles = expandProfiles(getActivatedProfiles(environment, binder, additionalProfiles));
        this.defaultProfiles = expandProfiles(getDefaultProfiles(environment, binder));
    }

    private List<String> getActivatedProfiles(Environment environment, Binder binder,
                                              Collection<String> additionalProfiles) {
        return asUniqueItemList(getProfiles(environment, binder, Type.ACTIVE), additionalProfiles);
    }

    private List<String> getDefaultProfiles(Environment environment, Binder binder) {
        return asUniqueItemList(getProfiles(environment, binder, Type.DEFAULT));
    }

    private Collection<String> getProfiles(Environment environment, Binder binder, Type type) {
        String environmentPropertyValue = environment.getProperty(type.getName());
        Set<String> environmentPropertyProfiles = (!StringUtils.hasLength(environmentPropertyValue))
                ? Collections.emptySet()
                : StringUtils.commaDelimitedListToSet(StringUtils.trimAllWhitespace(environmentPropertyValue));
        Set<String> environmentProfiles = new LinkedHashSet<>(Arrays.asList(type.get(environment)));
        BindResult<Set<String>> boundProfiles = binder.bind(type.getName(), STRING_SET);
        if (hasProgrammaticallySetProfiles(type, environmentPropertyValue, environmentPropertyProfiles,
                environmentProfiles)) {
            if (!type.isMergeWithEnvironmentProfiles() || !boundProfiles.isBound()) {
                return environmentProfiles;
            }
            return boundProfiles.map((bound) -> merge(environmentProfiles, bound)).get();
        }
        return boundProfiles.orElse(type.getDefaultValue());
    }

    private boolean hasProgrammaticallySetProfiles(Type type, String environmentPropertyValue,
                                                   Set<String> environmentPropertyProfiles, Set<String> environmentProfiles) {
        if (!StringUtils.hasLength(environmentPropertyValue)) {
            return !type.getDefaultValue().equals(environmentProfiles);
        }
        if (type.getDefaultValue().equals(environmentProfiles)) {
            return false;
        }
        return !environmentPropertyProfiles.equals(environmentProfiles);
    }

    private Set<String> merge(Set<String> environmentProfiles, Set<String> bound) {
        Set<String> result = new LinkedHashSet<>(environmentProfiles);
        result.addAll(bound);
        return result;
    }

    private List<String> expandProfiles(List<String> profiles) {
        Deque<String> stack = new ArrayDeque<>();
        asReversedList(profiles).forEach(stack::push);
        Set<String> expandedProfiles = new LinkedHashSet<>();
        while (!stack.isEmpty()) {
            String current = stack.pop();
            if (expandedProfiles.add(current)) {
                asReversedList(this.groups.get(current)).forEach(stack::push);
            }
        }
        return asUniqueItemList(expandedProfiles);
    }

    private List<String> asReversedList(List<String> list) {
        if (CollectionUtils.isEmpty(list)) {
            return Collections.emptyList();
        }
        List<String> reversed = new ArrayList<>(list);
        Collections.reverse(reversed);
        return reversed;
    }

    private List<String> asUniqueItemList(Collection<String> profiles) {
        return asUniqueItemList(profiles, null);
    }

    private List<String> asUniqueItemList(Collection<String> profiles, Collection<String> additional) {
        LinkedHashSet<String> uniqueItems = new LinkedHashSet<>();
        if (!CollectionUtils.isEmpty(additional)) {
            uniqueItems.addAll(additional);
        }
        uniqueItems.addAll(profiles);
        return Collections.unmodifiableList(new ArrayList<>(uniqueItems));
    }

    /**
     * Return an iterator for all {@link #getAccepted() accepted profiles}.
     */
    @Override
    public Iterator<String> iterator() {
        return getAccepted().iterator();
    }

    /**
     * Return the active profiles.
     *
     * @return the active profiles
     */
    public List<String> getActive() {
        return this.activeProfiles;
    }

    /**
     * Return the default profiles.
     *
     * @return the active profiles
     */
    public List<String> getDefault() {
        return this.defaultProfiles;
    }

    /**
     * Return the accepted profiles.
     *
     * @return the accepted profiles
     */
    public List<String> getAccepted() {
        return (!this.activeProfiles.isEmpty()) ? this.activeProfiles : this.defaultProfiles;
    }

    /**
     * Return if the given profile is active.
     *
     * @param profile the profile to test
     * @return if the profile is active
     */
    public boolean isAccepted(String profile) {
        return getAccepted().contains(profile);
    }

    @Override
    public String toString() {
        ToStringCreator creator = new ToStringCreator(this);
        creator.append("active", getActive().toString());
        creator.append("default", getDefault().toString());
        creator.append("accepted", getAccepted().toString());
        return creator.toString();
    }

    /**
     * A profiles type that can be obtained.
     */
    private enum Type {

        ACTIVE(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, Environment::getActiveProfiles, true,
                Collections.emptySet()),

        DEFAULT(AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME, Environment::getDefaultProfiles, false,
                Collections.singleton("default"));

        private final Function<Environment, String[]> getter;

        private final boolean mergeWithEnvironmentProfiles;

        private final String name;

        private final Set<String> defaultValue;

        Type(String name, Function<Environment, String[]> getter, boolean mergeWithEnvironmentProfiles,
             Set<String> defaultValue) {
            this.name = name;
            this.getter = getter;
            this.mergeWithEnvironmentProfiles = mergeWithEnvironmentProfiles;
            this.defaultValue = defaultValue;
        }

        String getName() {
            return this.name;
        }

        String[] get(Environment environment) {
            return this.getter.apply(environment);
        }

        Set<String> getDefaultValue() {
            return this.defaultValue;
        }

        boolean isMergeWithEnvironmentProfiles() {
            return this.mergeWithEnvironmentProfiles;
        }

    }

}